package com.swak.fx.support.ui;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;

import com.swak.fx.support.animation.AnimateFXInterpolator;
import com.swak.fx.support.animation.FadeIn;
import com.swak.fx.support.animation.FadeOut;

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.util.Duration;

/**
 * UIScrollPane.
 * 
 * @author 618lf
 */
public class UIScrollPane extends AnchorPane implements UILifecycle {

	// ================================================================================
	// Properties
	// ================================================================================
	private final String STYLE_CLASS = "ui-scroll-pane__wrap";
	private final String VBAR_STYLE_CLASS = "ui-scroll-pane__vbar";
	private final String STYLESHEET = UI.load("css/UIScrollPaneCus.css");

	private UIScrollPaneInner inner;

	public UIScrollPane() {
		this.inner = new UIScrollPaneInner();
		init();
	}

	// ================================================================================
	// Methods
	// ================================================================================
	@Override
	public void initializeParts() {
		getStyleClass().setAll(STYLE_CLASS);
		this.getChildren().add(inner);
		this.setPrefHeight(0);
		this.setPrefWidth(0);
		this.setMinHeight(0);
		this.setMinWidth(0);
		this.inner.setVbarPolicy(ScrollBarPolicy.NEVER);
		this.inner.setHbarPolicy(ScrollBarPolicy.NEVER);
		AnchorPane.setLeftAnchor(this.inner, 0.0);
		AnchorPane.setRightAnchor(this.inner, 0.0);
		AnchorPane.setTopAnchor(this.inner, 0.0);
		AnchorPane.setBottomAnchor(this.inner, 0.0);

		// vbar
		this.vbar.setVisible(false);
		this.vbar.getStyleClass().add(VBAR_STYLE_CLASS);
		this.vbar.setStyle("-fx-background-color: -thumb-color");

		this.getChildren().add(this.vbar);
		AnchorPane.setTopAnchor(this.vbar, 0.0);
		AnchorPane.setRightAnchor(this.vbar, 0.0);
	}

	private double yOffset = -1;
	private boolean dragging = false;

	@Override
	public void setupEventHandlers() {
		this.addEventHandler(MouseEvent.MOUSE_PRESSED, (e) -> {
			e.consume();
			Bounds vbarBounds = this.vbar.localToParent(this.vbar.getBoundsInLocal());
			this.yOffset = vbarBounds.getMinY() - e.getScreenY();
			this.dragging = true;
		});
		this.addEventHandler(MouseEvent.MOUSE_DRAGGED, (e) -> {
			e.consume();
			if (!this.dragging) {
				return;
			}
			double top = e.getScreenY() + this.yOffset;
			double change = top / this.getHeight();
			if (top + this.vbar.getHeight() >= this.getHeight()) {
				change = 1;
			}
			this.inner.vvalueProperty().set(change);
		});
		this.addEventHandler(MouseEvent.MOUSE_RELEASED, (e) -> {
			e.consume();
			this.dragging = false;
		});
		this.addEventHandler(MouseEvent.MOUSE_ENTERED, (e) -> {
			if (this.scrollHeight() > 0) {
				this.showBarProperty.set(true);
			} else {
				this.showBarProperty.set(false);
			}
		});
		this.addEventHandler(MouseEvent.MOUSE_EXITED, (e) -> {
			this.showBarProperty.set(false);
		});
	}

	@Override
	public void setupValueChangedListeners() {
		this.inner.vvalueProperty().addListener((ob, o, n) -> {
			double top = this.getHeight() * n.doubleValue();
			this.vbarTopProperty.set(top);
			this.showBarProperty.set(this.scrollHeight() <= 0 ? false : true);
		});
		this.vbarTopProperty.addListener((ob, o, n) -> {
			double top = n.doubleValue();
			if (top + this.vbar.getHeight() >= this.getHeight()) {
				top = this.getHeight() - this.vbar.getHeight();
			}
			AnchorPane.setTopAnchor(this.vbar, top);
		});
		this.vbarPolicyProperty.addListener((ob, o, n) -> {
			if (n == ScrollBarPolicy.ALWAYS) {
				this.vbar.setVisible(true);
			} else if (n == ScrollBarPolicy.NEVER) {
				this.vbar.setVisible(false);
			} else {
				this.vbar.setVisible(true);
				this.vbar.opacityProperty().set(0);
			}
		});
		this.showBarProperty.addListener((ob, o, n) -> {
			if (n && this.vbarPolicyProperty.get() == ScrollBarPolicy.AS_NEEDED && !this.dragging) {
				this.vbarFadeIn.play();
			} else if (this.vbarPolicyProperty.get() == ScrollBarPolicy.AS_NEEDED && !this.dragging) {
				this.vbarFadeOut.play();
			}
		});
	}

	@Override
	public void initializeFinish() {
		this.vbarPolicyProperty.set(ScrollBarPolicy.AS_NEEDED);
	}

	@Override
	public String getUserAgentStylesheet() {
		return STYLESHEET;
	}

	// ================================================================================
	// Funcs
	// ================================================================================
	public void setContent(Node node) {
		this.inner.setContent(node);
	}

	public Node getContent() {
		return this.inner.getContent();
	}

	public final void setVbarPolicy(ScrollBarPolicy value) {
		this.vbarPolicyProperty.set(value);
	}

	public final void setHbarPolicy(ScrollBarPolicy value) {
		// this.inner.setHbarPolicy(value);
	}

	public final void setFitToWidth(boolean value) {
		this.inner.setFitToWidth(value);
	}

	public UIScrollPaneInner getScroll() {
		return this.inner;
	}

	public DoubleProperty speed() {
		return this.inner.speed();
	}

	public DoubleProperty trackPadAdjustment() {
		return this.inner.trackPadAdjustment();
	}

	public ObjectProperty<Bounds> viewportBoundsProperty() {
		return this.inner.viewportBoundsProperty();
	}

	public DoubleProperty vvalueProperty() {
		return this.inner.vvalueProperty();
	}

	public void scrollToTop() {
		Timeline berthEventIn = new Timeline(
				new KeyFrame(Duration.millis(0),
						new KeyValue(this.inner.vvalueProperty(), this.inner.vvalueProperty().get(),
								AnimateFXInterpolator.EASE)),
				new KeyFrame(Duration.millis(200),
						new KeyValue(this.inner.vvalueProperty(), 0, AnimateFXInterpolator.EASE)));
		berthEventIn.play();
	}

	public void scrollToBottom() {
		this.scrollToBottom(200);
	}

	public void scrollToBottom(long timeMS) {
		if (timeMS > 0) {
			Timeline berthEventIn = new Timeline(
					new KeyFrame(Duration.millis(0),
							new KeyValue(this.inner.vvalueProperty(), this.inner.vvalueProperty().get(),
									AnimateFXInterpolator.EASE)),
					new KeyFrame(Duration.millis(timeMS),
							new KeyValue(this.inner.vvalueProperty(), 1, AnimateFXInterpolator.EASE)));
			berthEventIn.play();
		} else {
			this.inner.vvalueProperty().set(1);
		}
	}

	public void scrollToNode(Node node) {
		Bounds bounds = node.localToParent(node.getBoundsInLocal());
		double height = this.getHeight();
		this.inner.vvalueProperty().set(bounds.getMinY() / height);
	}

	public void scrollToBottom(double bottomHeight) {
		if (this.scrollHeight() > 0) {
			double vvalue = (this.scrollHeight() - bottomHeight) / this.scrollHeight();
			this.inner.vvalueProperty().set(vvalue);
		} else {
			this.inner.vvalueProperty().set(1);
		}
	}

	public double scrollHeight() {
		Node content = this.getContent();
		double scrollHeight = (content != null ? content.prefHeight(-1) : 0) - this.getHeight();
		return Math.max(0, scrollHeight);
	}

	public double scrollTop() {
		double value = this.inner.vvalueProperty().get();
		double scrollHeight = this.scrollHeight();
		return scrollHeight * value;
	}

	public double scrollBottom() {
		return this.scrollHeight() - scrollTop();
	}

	public boolean isScrollBottom(double adjustHeight) {
		return this.scrollBottom() <= adjustHeight;
	}

	// ================================================================================
	// Bar
	// ================================================================================
	private final Label vbar = new Label();
	private final FadeIn vbarFadeIn = new FadeIn(vbar);
	private final FadeOut vbarFadeOut = new FadeOut(vbar);
	private final ObjectProperty<ScrollBarPolicy> vbarPolicyProperty = new SimpleObjectProperty<ScrollPane.ScrollBarPolicy>(
			this, "vbar");
	private final DoubleProperty vbarTopProperty = new SimpleDoubleProperty(this, "vbarTop");
	private final BooleanProperty showBarProperty = new SimpleBooleanProperty(false);

	// ================================================================================
	// Loading
	// ================================================================================
	private Loading loading;

	public Loading getLoading() {
		if (loading == null) {
			loading = new Loading(this);
		}
		return loading;
	}

	public class Loading extends GridPane implements UILifecycle {

		private final Label text;
		private final UISpinner loading;
		private AtomicBoolean loadingFlag = new AtomicBoolean(false);
		private final UIScrollPane pane;

		Loading(UIScrollPane pane) {
			this.pane = pane;
			this.text = new Label("数据加载中...");
			this.loading = new UISpinner();
			this.loading.setRadius(8);
			this.init();
		}

		@Override
		public void initializeParts() {
			this.add(this.loading, 0, 0);
			this.add(this.text, 1, 0);

			GridPane.setHalignment(this.loading, HPos.RIGHT);
			GridPane.setHalignment(this.text, HPos.LEFT);
			GridPane.setValignment(this.loading, VPos.CENTER);
			GridPane.setValignment(this.text, VPos.CENTER);
			GridPane.setHgrow(this.loading, Priority.ALWAYS);
			GridPane.setHgrow(this.text, Priority.ALWAYS);
			GridPane.setVgrow(this.loading, Priority.ALWAYS);
			GridPane.setVgrow(this.text, Priority.ALWAYS);
			GridPane.setMargin(this.loading, new Insets(0, 2.5, 0, 0));
			GridPane.setMargin(this.text, new Insets(0, 0, 0, 2.5));

			// 添加到 pane 中;
			this.pane.getChildren().add(this);
			AnchorPane.setTopAnchor(this, 0.0);
			AnchorPane.setLeftAnchor(this, 0.0);
			AnchorPane.setRightAnchor(this, 0.0);
			this.setOpacity(0);
		}

		public boolean isLoading() {
			return this.loadingFlag.get();
		}

		private volatile CompletableFuture<Void> startFuture;

		public boolean start() {
			if (!loadingFlag.compareAndExchange(false, true)) {
				this.setOpacity(1);
				this.startFuture = new CompletableFuture<>();
				Timeline timeline = new Timeline(
						new KeyFrame(Duration.millis(0),
								new KeyValue(this.translateYProperty(), 0, AnimateFXInterpolator.EASE)),
						new KeyFrame(Duration.millis(1000),
								new KeyValue(this.translateYProperty(), 30, AnimateFXInterpolator.EASE)));
				timeline.setOnFinished((e) -> {
					this.startFuture.complete(null);
				});
				timeline.play();
				return true;
			}
			return false;
		}

		public void loaded() {
			if (this.startFuture != null) {
				this.startFuture.whenComplete((r, e) -> {
					Timeline timeline = new Timeline(
							new KeyFrame(Duration.millis(0),
									new KeyValue(this.translateYProperty(), this.translateYProperty().get(),
											AnimateFXInterpolator.EASE),
									new KeyValue(this.opacityProperty(), this.opacityProperty().get(),
											AnimateFXInterpolator.EASE)),
							new KeyFrame(Duration.millis(300),
									new KeyValue(this.translateYProperty(), 0, AnimateFXInterpolator.EASE),
									new KeyValue(this.opacityProperty(), 0, AnimateFXInterpolator.EASE)));
					timeline.setOnFinished((ex) -> {
						this.loadingFlag.set(false);
						this.startFuture = null;
					});
					timeline.play();
				});
			}
		}
	}
}